package panda.client.engine;

import java.util.ArrayList;
import java.util.List;

import panda.client.Cardpanda;
import panda.client.enumerations.CardScale;
import panda.client.enumerations.ClientID;
import panda.client.enumerations.Color;
import panda.client.listeners.CardDragHandler;
import panda.client.ui.observable.ObservableLastMousedCard;
import panda.client.ui.observable.ObservableLifeTotal;
import panda.client.ui.widgets.Bin;
import panda.client.ui.widgets.BinExplorer;
import panda.client.ui.widgets.Counter;
import panda.client.ui.widgets.DraggableCard;
import panda.client.ui.widgets.InPlay;
import panda.client.ui.widgets.LifeCounter;
import panda.client.ui.widgets.NoteTip;
import panda.client.ui.widgets.Tooltip;

import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.ui.AbsolutePanel;

/**
 * All the private crap from Panda should be in here to give it all a single point of access.
 * 
 * @author zane.tian@gmail.com
 */
public class ClientModel {
	private Bin p1gy, p1lib, p1hand, p2gy, p2lib, p2hand;
	private BinExplorer p1GyEx, p1LibEx, p1HandEx, p2GyEx, p2LibEx, p2HandEx;

	private final ArrayList<DraggableCard> draggableCardsInPlay = new ArrayList<DraggableCard>();
	private final ArrayList<Counter> counters = new ArrayList<Counter>();
	private final ObservableLastMousedCard lastMousedOver = new ObservableLastMousedCard();

	private ObservableLifeTotal p1ObsLife;
	private ObservableLifeTotal p2ObsLife;
	private LifeCounter p1LifeCounter;
	private LifeCounter p2LifeCounter;

	private long clientModelNum; /* keeps track of how many times modelnum has changed between server and client */
	private ClientID sessionID;

	public ClientModel(ClientID sessionID) {
		this.sessionID = sessionID;

		/* initialize all the bins now that we have the sessionID */
		p1gy = new Bin("Graveyard", ClientID.P1.getName());
		p1lib = new Bin("Library", ClientID.P1.getName());
		p1hand = new Bin("Hand", ClientID.P1.getName());

		p1HandEx = new BinExplorer(p1hand);
		p1LibEx = new BinExplorer(p1lib);
		p1GyEx = new BinExplorer(p1gy);

		p2gy = new Bin("Graveyard", ClientID.P2.getName());
		p2lib = new Bin("Library", ClientID.P2.getName());
		p2hand = new Bin("Hand", ClientID.P2.getName());

		p2HandEx = new BinExplorer(p2hand);
		p2LibEx = new BinExplorer(p2lib);
		p2GyEx = new BinExplorer(p2gy);

		p1ObsLife = new ObservableLifeTotal(ClientID.P1.getName());
		p2ObsLife = new ObservableLifeTotal(ClientID.P2.getName());

		p1LifeCounter = new LifeCounter(p1ObsLife);
		p2LifeCounter = new LifeCounter(p2ObsLife);

		AbsolutePanel canvas = Cardpanda.canvas;
		int canvasHeight = Cardpanda.canvasHeight;

		/*
		 * Two bin listeners are added in bin constructors and draggable card constructors: binMouseListenerForBins and
		 * binMouseListenerForCards. Arrange the top and bottom bins (top is one player, bottom is another) according to
		 * the sessionID. Generally we want bottom = current client, top = opponent. This follows well established design.
		 */

		/* need to initialize these guys here instead of in case:P1 because compiler complains they might not be init */
		Bin bottomGy = p1gy;
		Bin bottomLib = p1lib;
		Bin bottomHand = p1hand;
		LifeCounter bottomLifeCounter = p1LifeCounter;

		Bin topGy = p2gy;
		Bin topLib = p2lib;
		Bin topHand = p2hand;
		LifeCounter topLifeCounter = p2LifeCounter;

		/* this should technically be a if(sessionID.equals(ClientID.P2)) statement :P */
		if(Cardpanda.P2MIRROR) {
			switch(sessionID) {
				case OBSERVER:
				case P1:
					break;
				case P2:
					bottomGy = p2gy;
					bottomLib = p2lib;
					bottomHand = p2hand;
					bottomLifeCounter = p2LifeCounter;

					topGy = p1gy;
					topLib = p1lib;
					topHand = p1hand;
					topLifeCounter = p1LifeCounter;
					break;
				default:
					throw new IllegalStateException("There should not be a sessionID other than Observer, P1, or P2");
			}
		}

		canvas.add(bottomGy.getLabel(), Bin.canvasPadding, canvasHeight - Bin.height - Bin.canvasPadding);
		canvas.add(bottomLib.getLabel(), Bin.canvasPadding, canvasHeight - 2 * (Bin.height + Bin.canvasPadding));
		canvas.add(bottomHand.getLabel(), Bin.canvasPadding, canvasHeight - 3 * (Bin.height + Bin.canvasPadding));

		canvas.add(topGy.getLabel(), Bin.canvasPadding, Bin.canvasPadding);
		canvas.add(topLib.getLabel(), Bin.canvasPadding, Bin.height + 2 * Bin.canvasPadding);
		canvas.add(topHand.getLabel(), Bin.canvasPadding, 2 * Bin.height + 3 * Bin.canvasPadding);

		/* Add life counters here. */
		canvas.add(bottomLifeCounter, Bin.canvasPadding * 2 + Bin.width, canvasHeight - Bin.height - Bin.canvasPadding);
		canvas.add(topLifeCounter, Bin.canvasPadding * 2 + Bin.width, Bin.canvasPadding);

		loadBinExplorers(this.sessionID);
	}

	/** Increments this model's number by 1. */
	public void incrementModelNumber() {
		clientModelNum++;
	}
	
	/* used to update the model number after a model reset */
	public void setModelNumber(long number) {
		clientModelNum = number;
	}

	/* returns an int that is the mirror of y across the canvas. For DraggableCards, used for P2's mirroring */
	private int cardMirror(int y) {
		return Cardpanda.canvasHeight - y - CardScale.QUARTER.getHeight();
	}

	/**
	 * Returns an int that is the mirror of y across the canvas, except for a tapped card where it's height is an untaped
	 * card's width.
	 */
	private int tappedCardMirror(int y) {
		return Cardpanda.canvasHeight - y - CardScale.QUARTER.getWidth();
	}

	/* returns an int that is the mirror of y across the canvas. For Counters, used for P2's mirroring */
	private int counterMirror(int y) {
		return Cardpanda.canvasHeight - y - Counter.height;
	}

	/** Tells whether or not the server has new data for a target Bin */
	private boolean hasBinBeenUpdated(Bin clientBin, List<MagicCard> serverCards) {
		boolean changed = false;
		// System.out.println("current client size: " + client.size() + ". server size: " + server.size());

		if(clientBin.size() != serverCards.size()) {
			return true;
		} else {
			for(int i = 0; i < clientBin.getCards().size(); i++) { /* same size lists */
				MagicCard card = clientBin.getCards().get(i).getMagicCard();
				MagicCard serverCard = serverCards.get(i);

				// System.out.println("ID comparison - clientCard " + card.getNameAndServerID() + ". ServerCard: " +
				// serverCard.getNameAndServerID() );
				if(card.serverID != serverCard.serverID) {
					changed = true;
					break;
				}
			}
		}

		return changed;
	}

	/** Updates a bin with new server information if there is anything new. */
	private void updateBinIfChangesExist(Bin clientBin, List<MagicCard> serverCards) {
		if(hasBinBeenUpdated(clientBin, serverCards)) {
			clientBin.setBinContents(convertMagicToDrag(serverCards));
		}
	}

	/**
	 * Copies this model's data into a ServerModel object so it can be sent to the server.
	 */
	public ServerModel createServerModelCopy() {
		AbsolutePanel canvas = Cardpanda.canvas;
		ServerModel sendingToServer = new ServerModel();

		/* fill out the entire model except the inPlay cards */
		sendingToServer.setP1Life(p1ObsLife.getLifeTotal());
		sendingToServer.setP2Life(p2ObsLife.getLifeTotal());

		sendingToServer.setP1lib(p1lib.getMagicCardsSnapshot());
		sendingToServer.setP1gy(p1gy.getMagicCardsSnapshot());
		sendingToServer.setP1hand(p1hand.getMagicCardsSnapshot());

		sendingToServer.setP2lib(p2lib.getMagicCardsSnapshot());
		sendingToServer.setP2gy(p2gy.getMagicCardsSnapshot());
		sendingToServer.setP2hand(p2hand.getMagicCardsSnapshot());

		/* add the inPlay cards to the model */
		List<MagicCardAndPos> cardsAndPositions = new ArrayList<MagicCardAndPos>();
		for(DraggableCard card : draggableCardsInPlay) {
			int x = card.getAbsoluteLeft() - canvas.getAbsoluteLeft();
			int y = card.getAbsoluteTop() - canvas.getAbsoluteTop();

			switch(sessionID) {
				case OBSERVER: /* nothing special happens for observers */
				case P1:
					cardsAndPositions.add(new MagicCardAndPos(card.getMagicCard(), x, y));
					break;
				case P2: /* invert the y coordinate with a cardHeight offset */
					if(Cardpanda.P2MIRROR) {
						if(card.isTapped()) {
							cardsAndPositions.add(new MagicCardAndPos(card.getMagicCard(), x, tappedCardMirror(y)));
						} else {
							cardsAndPositions.add(new MagicCardAndPos(card.getMagicCard(), x, cardMirror(y)));
						}
					} else {
						cardsAndPositions.add(new MagicCardAndPos(card.getMagicCard(), x, y));
					}
					// card.getMagicCard(), x, Panda.canvasHeight - (y + CardScale.QUARTER.getHeight()
					break;
				default:
					throw new IllegalStateException("There shouldn't be any clientIDs other than P1, P2, and OBSERVER");
			}

		}
		sendingToServer.setInPlay(cardsAndPositions);

		/* add counters to the model */
		List<CounterAndPos> countersAndPositions = new ArrayList<CounterAndPos>();
		for(Counter counter : counters) {
			int x = counter.getAbsoluteLeft() - canvas.getAbsoluteLeft();
			int y = counter.getAbsoluteTop() - canvas.getAbsoluteTop();

			switch(sessionID) {
				case OBSERVER: /* fall through */
				case P1:
					countersAndPositions.add(new CounterAndPos(counter.getColor(), counter.getServerID(), x, y));
					break;
				case P2:
					if(Cardpanda.P2MIRROR) {
						countersAndPositions.add(new CounterAndPos(counter.getColor(), counter.getServerID(), x,
								counterMirror(y)));
					} else {
						countersAndPositions.add(new CounterAndPos(counter.getColor(), counter.getServerID(), x, y));
					}
					break;
				default:
					throw new IllegalStateException("There shouldn't be any clientIDs other than P1, P2, and OBSERVER");
			}
		}
		sendingToServer.setCounters(countersAndPositions);

		/* sync up the client's model number */
		sendingToServer.setModelNum(clientModelNum);

		/* Set model's updateMsg to the latest */
		sendingToServer.setUpdateMsg(NoteTip.get().getLatestUpdateForOtherClients());

		/* assert that there are no duplicate serverIDs anywhere */
		/* iterate through all bins and all cards in play */
		List<Long> serverIDs = new ArrayList<Long>();
		List<InPlay> binContents = new ArrayList<InPlay>();
		binContents.addAll(p1gy.getDraggableCardsSnapshot());
		binContents.addAll(p2gy.getDraggableCardsSnapshot());
		binContents.addAll(p1hand.getDraggableCardsSnapshot());
		binContents.addAll(p2hand.getDraggableCardsSnapshot());
		binContents.addAll(p1lib.getDraggableCardsSnapshot());
		binContents.addAll(p2lib.getDraggableCardsSnapshot());
		binContents.addAll(draggableCardsInPlay);
		binContents.addAll(counters);

		for(InPlay element : binContents) {
			Long serverID = element.getServerID();
			assert !serverIDs.contains(serverID) : "Duplicate ID found: " + element.getName() + " "
					+ element.getServerID();
			serverIDs.add(element.getServerID());
		}

		return sendingToServer;
	}

	/**
	 * Loads data from a ServerModel into this client's clientModel.
	 * 
	 * @param model
	 */
	public void syncWithServerModel(ServerModel model) {
		p1ObsLife.setLifeTotal(model.getP1Life());
		p2ObsLife.setLifeTotal(model.getP2Life());

		updateBinIfChangesExist(p1hand, model.getP1hand());
		updateBinIfChangesExist(p1lib, model.getP1lib());
		updateBinIfChangesExist(p1gy, model.getP1gy());
		updateBinIfChangesExist(p2hand, model.getP2hand());
		updateBinIfChangesExist(p2lib, model.getP2lib());
		updateBinIfChangesExist(p2gy, model.getP2gy());

		/* remove all cards in play */
		while(!draggableCardsInPlay.isEmpty()) {
			DraggableCard card = draggableCardsInPlay.get(0);
			removeCardInPlay(card);
		}

		/* refill draggableCardsInPlay, as well as canvas, with cards from the server's model.getInPlay() */
		for(MagicCardAndPos mcap : model.getInPlay()) {
			final DraggableCard card = new DraggableCard(mcap.getCard());
			
			DOM.setStyleAttribute(card.getElement(), "zIndex", String.valueOf(card.getZIndex()));

			switch(sessionID) {
				case OBSERVER: /* fall through */
				case P1:
					addCardInPlay(card, mcap.getX(), mcap.getY());
					break;
				case P2:
					if(Cardpanda.P2MIRROR) {
						if(card.isTapped()) {
							addCardInPlay(card, mcap.getX(), tappedCardMirror(mcap.getY()));
						} else {
							addCardInPlay(card, mcap.getX(), cardMirror(mcap.getY()));
						}
					} else {
						addCardInPlay(card, mcap.getX(), mcap.getY());
					}
					break;
				default:
					throw new IllegalStateException("There shouldn't be any clientIDs other than P1, P2, and OBSERVER");
			}

			/* if the card is tapped, make sure it has the tapped backgroundImage and the correct dimensions */
			if(card.isTapped()) {
				DOM.setStyleAttribute(card.getElement(), "width", CardScale.QUARTER.getHeight() + "px");
				DOM.setStyleAttribute(card.getElement(), "height", CardScale.QUARTER.getWidth() + "px");
				DOM.setStyleAttribute(card.getElement(), "backgroundImage", "url(" + card.getTapUrl() + ")");
			}
			
			/* if the card is face down, make sure the backgroundImage shows the m:tg card back */
			if(card.isFaceDown()) {
				if(card.isTapped()) {
					DOM.setStyleAttribute(card.getElement(), "backgroundImage", "url(" + card.getFaceDownTapUrl() + ")");
				} else {
					DOM.setStyleAttribute(card.getElement(), "backgroundImage", "url(" + card.getFaceDownImgUrl() + ")");
				}				
			}
		}

		/* remove all counters in play */
		while(!counters.isEmpty()) {
			Counter counter = counters.get(0);
			removeCounter(counter);
		}

		/* refill counters as well as canvas, with counters from the server's model.getCounters() */
		for(CounterAndPos cAndp : model.getCounters()) {
			Color color = cAndp.getColor();
			long serverID = cAndp.getServerID();

			switch(sessionID) {
				case OBSERVER: /* fall through */
				case P1:
					addCounter(color, serverID, cAndp.getX(), cAndp.getY());
					break;
				case P2:
					if(Cardpanda.P2MIRROR) {
						addCounter(color, serverID, cAndp.getX(), counterMirror(cAndp.getY()));
					} else {
						addCounter(color, serverID, cAndp.getX(), cAndp.getY());
					}
					break;
				default:
					throw new IllegalStateException("There shouldn't be any clientIDs other than P1, P2, and OBSERVER");
			}
		}

		/* Update the NoteTip to display the model's latest updateMsg */
		NoteTip.get().logPersonalNote(model.getUpdateMsg(), false);

		/* sync up the client's model number */
		clientModelNum = model.getModelNum();
		
		/* make sure the focus is set on cardpanda's main focus panel */
		Cardpanda.setFocus();
		
		/* remove the tooltip, in case we were hovering over a card that is now moved or gone */
		DeferredCommand.addCommand(new Command() {
			public void execute() {
				Tooltip.get().hide();
			}
		});
	}

	public List<Bin> getBinsThisClientControls() {
		List<Bin> bins = new ArrayList<Bin>();

		switch(sessionID) {
			case OBSERVER:
				break;
			case P1:
				bins.add(p1gy);
				bins.add(p1lib);
				bins.add(p1hand);
				break;
			case P2:
				bins.add(p2gy);
				bins.add(p2lib);
				bins.add(p2hand);
				break;
			default:
				throw new IllegalStateException("There shouldn't be any clientIDs other than P1, P2, and OBSERVER");
		}

		return bins;
	}

	public void createCounter(Color color, int x, int y) {
		Cardpanda.createCounter(color, counters, x, y);
	}

	/** Adds a counter to the canvas and the counters (in play) list. */
	private void addCounter(Color color, long serverID, int x, int y) {
		Counter counter = new Counter(color, serverID);

		counters.add(counter);
		Cardpanda.canvas.add(counter, x, y);

		System.out.println(counter.getName() + " " + counter.getServerID() + " added at " + x + "," + y);
	}

	/** Removes a counter from the canvas and counters (in play) list. */
	public void removeCounter(Counter counter) {
		counters.remove(counter);
		Cardpanda.canvas.remove(counter);
	}

	/**
	 * Adds the card to the canvas and inPlay list and sets its z-index to the appropriate value. Calls
	 * DragListener.place so its snap coordinates are set properly.
	 */
	public void addCardInPlay(DraggableCard card, int x, int y) {
		// System.out.println("before adding " + card.getName() + " dcip size is " + draggableCardsInPlay.size());

		/** consistency check here to make sure no duplicate IDs exist in either canvas or dcip: bad performance */
		for(DraggableCard drag : draggableCardsInPlay) {
			if(drag.getMagicCard().getServerID() == card.getMagicCard().getServerID()) {
				throw new IllegalStateException("Revealing a card: " + card.getName()
						+ " with the same ID as one already in play: " + card.getName());
			}
		}

		draggableCardsInPlay.add(card);
		Cardpanda.canvas.add(card, x, y);
		DOM.setStyleAttribute(card.getElement(), "zIndex", String.valueOf(card.getZIndex()));
		CardDragHandler.placeSnaps(card);
	}

	/**
	 * Removes a card from the InPlay area. Removes its snaps too. Impl-wise, this removes the DraggableCard obj from
	 * both Panda.canvas and Panda.draggableCardsInPlay.
	 * 
	 * @param card
	 */
	public void removeCardInPlay(DraggableCard card) {
		boolean cardExistsInPlay = Cardpanda.canvas.remove(card);
		if(!cardExistsInPlay) {
			throw new IllegalStateException("Card " + card.getName() + " was not removed from play");
		}
		
		draggableCardsInPlay.remove(card);

		/**
		 * remove the snap position from DragListener's static snapMap. This is so there isn't snapping to 'invisible'
		 * cards near bins where they were added to that bin.
		 */
		CardDragHandler.removeSnaps(card);
	}

	/** Toggles library visibility and updates the server so all clients know. */
	public void toggleLibraryVisibility() {
		String name = Cardpanda.clientModel.getSessionID().getName();
		String logForMe = "uninitialized";
		String logForOthers = "uninitialized";

		switch(sessionID) {
			case P1:
				if(p1LibEx.isVisible()) {
					/* if the library is visible, toggling will hide it. */
					logForMe = "You close your library.";
					logForOthers = name + " closes " + name + "'s library.";
				} else {
					/* library is not visible. toggling will make it visible */
					logForMe = "You look through your library.";
					logForOthers = name + " looks through " + name + "'s library.";
				}

				p1LibEx.toggleVisibility();
				NoteTip.get().log(logForMe, logForOthers);
				break;

			case P2:
				if(p2LibEx.isVisible()) {
					/* if the library is visible, toggling will hide it. */
					logForMe = "You close your library.";
					logForOthers = name + " closes " + name + "'s library.";
				} else {
					/* library is not visible. toggling will make it visible */
					logForMe = "You look through your library.";
					logForOthers = name + " looks through " + name + "'s library.";
				}

				p2LibEx.toggleVisibility();
				NoteTip.get().log(logForMe, logForOthers);
				break;

			default: /* anyone can be updated that a client looks through his library but only P1 and P2 can look */
		}

		NoteTip.get().log(logForMe, logForOthers);

		Cardpanda.sendModelUpdateToServer(sessionID);
	}

	/** Toggles graveyard visibility. Noone needs to know a client is looking through his graveyard. */
	public void toggleGraveyardVisibility() {
		switch(sessionID) {
			case P1:
				p1GyEx.toggleVisibility();
				NoteTip.get().logPersonalNote("toggling Graveyard Explorer visibility", true);
				break;

			case P2:
				p2GyEx.toggleVisibility();
				NoteTip.get().logPersonalNote("toggling Graveyard Explorer visibility", true);
				break;

			default: // do nothing;
		}
	}

	/** Draw 7 cards. */
	public void drawSevenCards() {
		switch(sessionID) {
			case P1:
				/* fall through because action is the same for P1 and P2 */
			case P2:
				// emptyHandAndGyIntoLib();
				shuffleLibrary();

				for(int i = 0; i < 7; i++) {
					drawCardFromLibrary(false);
				}
				break;
			default: // do nothing;
		}
	}

	/** Shuffles target player's hand+gy into their library and draws 7 new cards. Does not affect any cards in play */
	public int Mulligan() {
		Bin currentHand = null;
		switch(sessionID) {
			case P1:
				currentHand = p1hand;
				break;
			case P2:
				currentHand = p2hand;
				break;
			default: // do nothing;
		}

		int handSize = currentHand.size();

		emptyHandAndGyIntoLib();
		shuffleLibrary();
		for(int i = 0; i < handSize - 1; i++) {
			drawCardFromLibrary(false);
		}
		
		updateServer();

		return currentHand.size();
	}

	/** Toggles the tapped status of a card. */
	public void tapToggle(DraggableCard card) {
		DraggableCard tapTarget = card;
		if(!tapTarget.isDragging()) {
			if(tapTarget.isTapped()) {
				tapTarget.untapWhileInPlay();

				String logForMe = "You untap " + tapTarget.getName() + ".";
				String logForOthers = Cardpanda.clientModel.getSessionID().getName() + " untaps " + tapTarget.getName()
						+ ".";
				NoteTip.get().log(logForMe, logForOthers);
			} else {
				tapTarget.tapWhileInPlay();

				String logForMe = "You tap " + tapTarget.getName() + ".";
				String logForOthers = Cardpanda.clientModel.getSessionID().getName() + " taps " + tapTarget.getName() + ".";
				NoteTip.get().log(logForMe, logForOthers);
			}
		} /* else CanvasLog.get().log("no card 'last moused over'. can't tap"); */
	}
	
	/* Untaps all tapped cards you own on the board */
	public void untapOwnedPermanents() {
		for(DraggableCard card : draggableCardsInPlay) {
			if(card.isTapped() && card.isMine()) {
				card.untapWhileInPlay();
			}
		}
	}

	/** Takes the contents of the client's hand and graveyard and puts it all into the client's library. */
	public void emptyHandAndGyIntoLib() {
		switch(sessionID) {
			case P1:
				while(!p1hand.isEmpty()) {
					p1lib.addCardToTop(p1hand.removeTopCard());
				}

				while(!p1gy.isEmpty()) {
					p1lib.addCardToTop(p1gy.removeTopCard());
				}

				break;
			case P2:
				while(!p2hand.isEmpty()) {
					p2lib.addCardToTop(p2hand.removeTopCard());
				}

				while(!p2gy.isEmpty()) {
					p2lib.addCardToTop(p2gy.removeTopCard());
				}

				break;
			default: // do nothing
		}
	}

	/** Reveals a card from current player's hand at random. */
	public void revealRandomCardInHand(boolean verbose) {
		switch(sessionID) {
			case P1:
				revealRandomHelper(p1hand, verbose);
				break;

			case P2:
				revealRandomHelper(p2hand, verbose);
				break;

			default: // do nothing
		}
	}
	
	/** helper function for revealRandomCardInHand(); deals with duplicate code */
	private void revealRandomHelper(Bin hand, boolean verbose) {
		if(hand.isEmpty()) {
			NoteTip.get().logPersonalNote(
					"You cannot reveal a random card from your hand because your hand is empty.", true);
		} else {
			int randomPosition = getRandInt(hand.getCards().size() - 1);
			DraggableCard removed = hand.removeCardAtIndex(randomPosition);

			Cardpanda.clientModel.addCardInPlay(removed, 40, Cardpanda.canvasHeight / 2 - CardScale.QUARTER.getHeight()
					/ 2);

			if(verbose) {
				String user = Cardpanda.clientModel.getSessionID().getName();
				String logForMe = "You reveal " + removed.getName() + " at random from your hand. Current hand size: "
						+ hand.size();
				String logForOthers = user + " reveals " + removed.getName() + " at random from " + user
						+ "'s hand. Current hand size: " + hand.size();
				NoteTip.get().log(logForMe, logForOthers);
			}
			
			updateServer();
		}
	}

	/** Puts a card at the bottom of user's library */
	public void putCardAtLibraryBottom(boolean verbose) {
		switch(sessionID) {
			case P1: 
				putCardAtLibraryBottomHelper(p1lib, verbose);
				break;
			case P2:
				putCardAtLibraryBottomHelper(p2lib, verbose);
				break;
			default: // do nothing
		}
	}
	
	private void putCardAtLibraryBottomHelper(Bin library, boolean verbose) {
		ObservableLastMousedCard lastMousedOver = Cardpanda.clientModel.getObsLastMousedCard();
		
		if(!lastMousedOver.hasCard() || !inPlay(lastMousedOver.getCard())) {
			NoteTip.get().logPersonalNote("Select a card (in play) to put on the bottom of your library.", true);
			return;
		} else {

			/* order is important here because p1lib.insertCard modifies the card to be face up */
			DraggableCard removed = lastMousedOver.getCard();

			if(removed.getMagicCard() instanceof MagicCardToken) {
				/* for tokens, remove the token from play. */
				Cardpanda.clientModel.removeCardInPlay(removed);

				String end = "Because it is a token, it is removed from the game.";
				String logForMe = "You put " + removed.getName() + " on the bottom of your library. " + end;
				String logForOthers = removed + " puts " + removed.getName() + " on the bottom of your library. " + end;
				NoteTip.get().log(logForMe, logForOthers);
			} else {

				if(verbose) {
					String user = Cardpanda.clientModel.getSessionID().getName();
					String logForMe = "You put " + removed.getName() + " at the bottom of your library.";
					String logForOthers = user + " puts a card at the bottom of " + user + "'s library.";
					NoteTip.get().log(logForMe, logForOthers);
				}

				removeCardInPlay(removed);
				Tooltip.get().hide();
				library.insertCard(removed, library.size());
			}
			updateServer();
		}
	}

	/** Target player reveals a card from his library unless it is empty. can reveal face down. */
	public void revealTopCardOfLibrary(boolean verbose, boolean isShiftKeyDown) {
		switch(sessionID) {
			case P1:
				revealTopCardOfLibHelper(verbose, isShiftKeyDown, p1lib);
				break;

			case P2:
				revealTopCardOfLibHelper(verbose, isShiftKeyDown, p2lib);
				break;

			default: // do nothing
		}
	}
	
	private void revealTopCardOfLibHelper(boolean verbose, boolean isShiftKeyDown, Bin library) {
		if(library.isEmpty()) {
			NoteTip.get().logPersonalNote("You cannot reveal the top card of your library because it is empty.", true);
			return;
		} else {
			DraggableCard removed = library.removeTopCard();
			
			if(isShiftKeyDown) {
				removed.flipOver();
			}
			
			Cardpanda.clientModel.addCardInPlay(removed, 40, Cardpanda.canvasHeight / 2
					- CardScale.QUARTER.getHeight() / 2);

			if(verbose) {
				String cardName = removed.getName();
				
				String user = Cardpanda.clientModel.getSessionID().getName();
				String logForMe = "You reveal " + cardName + " from the top of your library.";
				String logForOthers = user + " reveals " + cardName + " from the top of " + user
						+ "'s library.";
				NoteTip.get().log(logForMe, logForOthers);
			}

			updateServer();
		}
	}

	/** Target player draws a card unless his or her library is empty. */
	public void drawCardFromLibrary(boolean verbose) {
		switch(sessionID) {
			case P1:
				if(p1lib.isEmpty()) {
					NoteTip.get().logPersonalNote("You cannot draw a card because your library is empty.", true);
					return;
				} else {
					DraggableCard removed = p1lib.removeTopCard();
					p1hand.addCardToTop(removed);

					if(verbose) {
						String logForMe = "You draw " + removed.getName() + ". Current hand size: " + p1hand.size();
						String logForOthers = Cardpanda.clientModel.getSessionID().getName()
								+ " draws a card. Current hand size: " + p1hand.size();
						NoteTip.get().log(logForMe, logForOthers);
						updateServer();
					}
				}
				break;

			case P2:
				if(p2lib.isEmpty()) {
					NoteTip.get().logPersonalNote("You cannot draw a card because your library is empty.", true);
					return;
				} else {
					DraggableCard removed = p2lib.removeTopCard();
					p2hand.addCardToTop(removed);

					if(verbose) {
						String logForMe = "You draw " + removed.getName() + ". Current hand size: " + p2hand.size();
						String logForOthers = Cardpanda.clientModel.getSessionID().getName()
								+ " draws a card. Current hand size: " + p2hand.size();
						NoteTip.get().log(logForMe, logForOthers);
						updateServer();
					}
				}
				break;

			default: // do nothing
		}
	}

	/** Shuffles the client's library. */
	public void shuffleLibrary() {
		String logForMe = "You shuffle your library.";
		String logForOthers = Cardpanda.clientModel.getSessionID().getName() + "'s library has been shuffled.";

		switch(sessionID) {
			case P1:
				p1lib.shuffle();
				NoteTip.get().log(logForMe, logForOthers);
				break;

			case P2:
				p2lib.shuffle();
				NoteTip.get().log(logForMe, logForOthers);
				break;

			default: // do nothing
		}
	}

	/* returns true if target card is in play. */
	public boolean inPlay(DraggableCard card) {
		return draggableCardsInPlay.contains(card);
	}

	/* accessors */
	public void setLastMousedOverCard(DraggableCard card) {
		// System.out.println("[c] clientModel.setlastmousedovercard - setting " + card.getName());
		lastMousedOver.setCard(card);
	}

	public ObservableLastMousedCard getObsLastMousedCard() {
		return lastMousedOver;
	}

	public List<Counter> getCounters() {
		return counters;
	}

	public ClientID getSessionID() {
		return sessionID;
	}

	public long getClientModelNum() {
		return clientModelNum;
	}

	/** Creates a List<DraggableMagicCard> from a List<MagicCard> */
	private List<DraggableCard> convertMagicToDrag(List<MagicCard> cards) {
		List<DraggableCard> result = new ArrayList<DraggableCard>();
		for(MagicCard card : cards) {
			result.add(new DraggableCard(card));
		}

		return result;
	}

	/** Sets the bin explorers in their default starting positions */
	private void loadBinExplorers(ClientID clientID) {
		AbsolutePanel canvas = Cardpanda.canvas;
		switch(clientID) {
			case P1:
				canvas.add(p1HandEx, 300, 50);
				canvas.add(p1LibEx, 50, 80);
				p1LibEx.hide();
				canvas.add(p1GyEx, 200, 110);
				p1GyEx.hide();

				canvas.add(p2HandEx, 300, 400);
				p2HandEx.hide();
				canvas.add(p2LibEx, 150, 80);
				p2LibEx.hide();
				canvas.add(p2GyEx, 300, 110);
				p2GyEx.hide();
				break;

			case P2:
				canvas.add(p2HandEx, 300, 50);
				canvas.add(p2LibEx, 50, 80);
				p2LibEx.hide();
				canvas.add(p2GyEx, 200, 110);
				p2GyEx.hide();

				canvas.add(p1HandEx, 300, 400);
				p1HandEx.hide();
				canvas.add(p1LibEx, 150, 80);
				p1LibEx.hide();
				canvas.add(p1GyEx, 300, 110);
				p1GyEx.hide();
				break;

			default: // do nothing
		}
	}

	/** Returns a random integer in the range [0, max] */
	public int getRandInt(int max) {
		/* java.util.Random (.nextInt(n)) is not supported by GWT, but it gives [0, max - 1], which is better */
		return (int) Math.round(Math.random() * max);
	}

	private void updateServer() {
		Cardpanda.sendModelUpdateToServer(Cardpanda.clientModel.getSessionID());
	}
}